package spl.army;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.ParseException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.*;

import spl.utils.FileProperties;
import spl.utils.GlobalLogger;

public class Warehouse {
	private static HashMap<String,ItemContainer> items = new HashMap<String, ItemContainer>();
	
	// Holds a lock for the entire warehouse.
	// This lock is used only when reading all warehouse information.
	private final static ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
	
	// Switch lock: allow multiple "writers", and one "reader"
	// This is O.K, because multiple writers, means, multiple threads
	// writing at a time, to a different ItemContainer, where as one reader means
	// only one thread will be allowed to query all of the ItemContainers, while ensuring
	// no thread is using the container.
	private final static Lock READ_LOCK = READ_WRITE_LOCK.writeLock();
	private final static Lock WRITE_LOCK = READ_WRITE_LOCK.readLock();

	private static final long GIVEUP_TIMEOUT = 500;
	
	/**
	 * disable this constructor
	 */
	private Warehouse(){}
	
	/**
	 * Clears all items
	 */
	public static void resetWarehouse() {
		Warehouse.items = new HashMap<String, ItemContainer>();
	}
	
	/**
	 * Parse config file
	 * @param warehousePropFile path on disk
	 * @throws ParseException when file's format is incorrect
	 * @throws FileNotFoundException when file is not found on disk
	 * @throws IOException on other IO error
	 */
	public static void initWarehouseFromFile(String warehousePropFile) throws ParseException, FileNotFoundException, IOException
	{
		
		FileProperties fp = null;
		fp = new FileProperties(warehousePropFile);
		String numberOfItemsStr = fp.getProperty("numberOfItems");
		if(null==numberOfItemsStr) {
			numberOfItemsStr = fp.getProperty("numberofItems");
			if(null==numberOfItemsStr) {
				throw new ParseException("no attribute 'numberofItems' in the given properties file", 0);
			}
		}
		
		Integer numberOfItems = Integer.decode(numberOfItemsStr.trim());
		
		for(int i=0; i<numberOfItems; i++){
			String name = fp.getProperty(i, "item", "Name");
			String amount = fp.getProperty(i, "item", "Amount");
			Warehouse.addItem(name, Integer.decode(amount.trim()));
		}
	}
	
	private static void updateWarehouse() {
		// Update chief, in case it's sleeping because non of the
		// sergeant could have acquired unlisted item
		ChiefOfStaff.updateFromBoard();
	}
	
	/**
	 * Adding item to the warehouse.
	 * @param name of item
	 * @param amount of item to add
	 */
	public static void addItem(String name, int amount) 
	{
		GlobalLogger.getLogger().finest("Adding item " + name + " x " + amount + " to warehouse");
		
		// If item already exist, add it it's container synchronously
		if(Warehouse.items.containsKey(name)){
			ItemContainer current = Warehouse.items.get(name);
			// Blocks until no one uses this specific item
			current.returnAmount(amount);
		}else{
			// If it's a new item, simply add it, as probably no one is trying
			// to acquire non-existing item
			ItemContainer newItem = new ItemContainer(name,amount);
			Warehouse.items.put(name, newItem);
		}
		
		Warehouse.updateWarehouse();
	}
	
	/**
	 * adds item
	 * @param name of item
	 * @param amount of item to add
	 */
	public static void addItem(String name, String amount) {
		Warehouse.addItem(name, Integer.decode(amount));
	}
	
	/**
	 * take a single item required from warehouse. that item would hold
	 * the name of the required item and it's amount.
	 * @param itemRequired is a container to the item of the amount required
	 * @param sergeant that consumes it.
	 * @return Item container of item's name and the amount allocated
	 * @throws TimeoutException 
	 * @throws InterruptedException 
	 */
	public static Item takeItem(ItemRequired itemRequired, Sergeant sergeant) throws TimeoutException, InterruptedException {
		return Warehouse.takeItemAsAmount(itemRequired.getName(),
											itemRequired.getAmount(), sergeant);
	}
	
	/**
	 * Get amount of items 'name' that currently are in the warehouse.
	 * @param name of item
	 * @return items amount
	 */
	public static int getItemCurrentAmount(String name)
	{
		ItemContainer currentItem = Warehouse.items.get(name);
		return currentItem.getAmount();
	}

	/**
	 * Decrement a specified amount of items of 'name' from warehouse
	 * @param name
	 * @param amount
	 * @param sergeant 
	 * @return Item
	 * @throws TimeoutException 
	 * @throws InterruptedException 
	 */
	private static Item takeItemAsAmount(String name, int amount, Sergeant sergeant) throws TimeoutException, InterruptedException
	{
		GlobalLogger.getLogger().finest("Taking " + name + " x " + amount  +" from warehouse");

		ItemContainer currentItem = items.get(name);
		
		// Make sure that item is taken in a synchronized way.
		// This will cause all the Mission Tasks to wait, in case 
		// item is not available in the warehouse
		Item t;
		synchronized(currentItem){
			// Notice that we are already locked on currentItem, so takeAmount()'s sync
			// will not affect the liveness of this thread.
			t = currentItem.takeAmount(amount, sergeant);
			if (t == null) {
				GlobalLogger.getLogger().info("Waiting for " + currentItem.toString() + " to become available");
				currentItem.wait(Warehouse.GIVEUP_TIMEOUT);
				// Try to fetch item again
				t = currentItem.takeAmount(amount, sergeant);
				if(null == t)
					throw new TimeoutException("We don't want to have spare items when we don't use them!");
			}
		}
		return t;
	}
	
	/**
	 * returns items to warehouse
	 * @param returnedItems is a list of items we return
	 * @param sergeant is the sergeant returning the items
	 */
	public static void returnItems(List<Item> returnedItems, Sergeant sergeant) {
        Iterator<Item> it = returnedItems.iterator();
        // Return all items
        while(it.hasNext()){
        	Item current = it.next();

        	GlobalLogger.getLogger().finest("Returning " + current.toString());
        	// Notice that this function is synched on the ItemContainer
        	// that is associated with current item. it means that only one thread at
        	// a time will be able to returnItems.
        	current.returnToWarehouse();
        }
	}
	
	/**
	 * reportWarehouse results a valid list of all items in warehouse,
	 * and their consumers.
	 * It locks all the warehouse (items) as it generates the report.
	 * This shouldn't cause liveness issues, because it is done only upon
	 * explicit requests
	 */
	public static void reportWarehouse() {
		Formatter formatter = new Formatter();
		
		// Only one thread can have this lock
		Warehouse.READ_LOCK.lock(); 
		GlobalLogger.getLogger().info("Locking Warehouse to generate report");
		
		Warehouse.items.entrySet();
		ItemContainer itmCon;
		for (Map.Entry<String, ItemContainer> itm : Warehouse.items.entrySet()) {
			itmCon = itm.getValue(); 
			
			formatter.format("[+] %s. Currently: %d\n", itmCon.toString(), itmCon.getAmountAssynchly());
			for (Map.Entry<Sergeant, Integer> holder : itmCon.getHoldersAssynchronously().entrySet()){
				if (holder.getKey() != null) {
					formatter.format("\t%s: %d\n", holder.getKey().getName(),
													Math.abs(holder.getValue().intValue()));
				}
			}
		}

		// Unlock warehouse.
		GlobalLogger.getLogger().info("Unlocking Warehouse");
		Warehouse.READ_LOCK.unlock();
		
		System.out.println(formatter.toString());
	}
	
	/**
	 * When an ItemContainer is performing allocation or deallocation
	 * of items to some sergeant, it locks the function reportWarehouse.
	 * The idea is that anyone can have access to WRITE_LOCK, which is actually
	 * a ReadWrite.ReadLock(), because different containers can change the amount
	 * of items they hold, at a same time. But only when reading the warehouse (like
	 * in reportWarehouse), item containers must not be changed.
	 * So only one reader can work while all writers cannot. And multiple writers can
	 * work at the same time.
	 * See wareHouseUnlock().
	 */
	public static void wareHouseLock() {
		Warehouse.WRITE_LOCK.lock();
	}

	/**
	 * When an ItemContainer is performing allocation or deallocation
	 * of items to some sergeant, it locks the function reportWarehouse function.
	 * See wareHouseLock  
	 */
	public static void wareHouseUnlock() {
		Warehouse.WRITE_LOCK.unlock();
	}
	
	
}

